﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Xml.Linq;
using Xom.Attributes;
using Xom.Elements;
using Xom.Utilities;

namespace Xom.Engine
{
    public class XDocumentEngine: XomEngine
    {
        private XDocument _xmlDocument;

        /// <summary>
        /// Notificate if the content of the xml file has changed
        /// </summary>
        public delegate void XmlDocumentChangedEventHandler(XDocument xmlFile);

        /// <summary>
        /// Notificate if the content of the xml file has changed
        /// </summary>
        public event XmlDocumentChangedEventHandler OnXmlDocumentContentChanged;

        /// <summary>
        /// Loads the XOM from the specified URL.
        /// </summary>
        /// <param name="filename">A URI string that references the file to load into a new XOM.</param>
        /// <param name="type">The type of the object that this XOM can serialize.</param>
        /// <returns>An XOM Object that contains the contents of the specified file.</returns>
        public object Load(string filename, Type type)
        {
            _xmlDocument = XDocument.Load(filename);
            _xmlDocument.Changed += RaiseXmlContentChanged;
            return ReadXml(_xmlDocument.Root, type);
        }

        /// <summary>
        /// Create a XOM Object from a specified XDocument.
        /// </summary>
        /// <param name="xmlfile">A XDocument that references the file to load into a new XOM Object.</param>
        /// <param name="type">The type of the object that this XOM can serialize.</param>
        /// <returns>An XOM Object that contains the contents of the specified file.</returns>
        public object Load(XDocument xmlfile, Type type)
        {
            _xmlDocument = xmlfile;
            _xmlDocument.Changed += RaiseXmlContentChanged;
            return ReadXml(_xmlDocument.Root, type);
        }

        /// <summary>
        /// Create a XOM Object from a specified XElement.
        /// </summary>
        /// <param name="xmlObject">A XElement that references the Element to load into a new XOM Object.</param>
        /// <param name="type">The type of the object that this XOM can serialize.</param>
        /// <returns>An XOM Object that contains the contents of the specified file.</returns>
        public object Load(XElement xmlObject, Type type)
        {
            //no extra event -> handled by caller of the function
            //xmlObject.Changed += RaiseXmlContentChanged;
            return ReadXml(xmlObject, type);
        }

        /// <summary>
        /// Creates a new XOM from a string.
        /// </summary>
        /// <param name="text">A string that contains XML.</param>
        /// <param name="type">The type of the object that this XOM can serialize.</param>
        /// <returns>An XOM Object populated from the string that contains XML.</returns>
        public object Parse(string text, Type type)
        {
            _xmlDocument = XDocument.Parse(text);
            _xmlDocument.Changed += RaiseXmlContentChanged;
            return ReadXml(_xmlDocument.Root, type);
        }

        /// <summary>
        /// Serialize the loaded XML to a String.
        /// </summary>
        /// <returns>A String that the XML will be written to.</returns>
        public string Save()
        {
            if (_xmlDocument == null) return null;
            StringBuilder sb = new StringBuilder();
            TextWriter tr = new StringWriter(sb); 
            _xmlDocument.Save(tr);

            return sb.ToString();
        }

        /// <summary>
        /// Save the loaded XML document to the specified file.
        /// </summary>
        /// <param name="filename">The location of the file where you want to save the document.</param>
        public void Save(string filename)
        {
            if (_xmlDocument == null) return;
            _xmlDocument.Save(filename);
        }

        /// <summary>
        /// Create a new XML document from a specified XOM Object.
        /// </summary>
        /// <param name="element">An XOM Object that contains the contents for the specified file.</param>
        /// <param name="filename">The location of the file where you want to save the document.</param>
        /// <param name="rootNodeName">The name of the root XML node.</param>
        public void Create(IXomElement element, string filename, string rootNodeName)
        {
            _xmlDocument = CreateDocument(element, rootNodeName);
            _xmlDocument.Save(filename);
        }

        /// <summary>
        /// Creates a new XDocument from a specified Object
        /// </summary>
        public XDocument CreateDocument(IXomElement xmlObject, string rootName)
        {
            XDocument newXmlFile = new XDocument();
            XElement newXmlRoot = CreateElement(xmlObject, rootName);
            newXmlFile.Add(newXmlRoot);

            return newXmlFile;
        }

        /// <summary>
        /// Creates a new XElement from a specified Object
        /// </summary>
        public XElement CreateElement(IXomElement xmlObject, string rootName)
        {
            return CreateXElementFromObject(xmlObject, rootName);
        }

        private void RaiseXmlContentChanged(object sender, XObjectChangeEventArgs e)
        {
            if (OnXmlDocumentContentChanged != null) OnXmlDocumentContentChanged(_xmlDocument);
        }

        private object ReadXml(XElement xml, Type type)
        {
            if (xml == null) return null;
            if (type == null) return null;

            IXomElement element = CreateNewXomElement(type) as IXomElement;
            ReadXmlProperties(xml, element, type);

            return element;
        }

        private void ReadXmlProperties(XElement xml, IXomElement element, Type type)
        {
            //get all property informations
            IList<MappedPropertyInfo> properties = GetOrAddTypeProperties(type);

            foreach (MappedPropertyInfo propertyInfo in properties)
            {
                XObject xmlObject = null;
                if (!string.IsNullOrEmpty(propertyInfo.Binding.XmlName))
                {
                    if (propertyInfo.Binding.IsAttribute) xmlObject = xml.Attribute(propertyInfo.Binding.XmlName);
                    else xmlObject = xml.Element(propertyInfo.Binding.XmlName);
                }
                else
                {
                    xmlObject = xml;
                }

                //can't find property in xml
                if (xmlObject == null)
                {
                    if (propertyInfo.Binding.IsRequired) Trace.TraceWarning("Can't find xml element " + propertyInfo.Binding.XmlName + " in " + xml.Name);
                    //add property mapping with empty node reference
                    element.Properties.Add(propertyInfo.Info.Name, new XomPropertyInfo<XObject>(propertyInfo.Info, new XomNode<XObject>(null)));
                    continue;
                }

                string assingableValue = string.Empty;
                if (propertyInfo.Binding.IsAttribute) assingableValue = ((XAttribute)xmlObject).Value;
                else assingableValue = ((XElement)xmlObject).Value;

                if (TypeUtility.IsSimpleType(propertyInfo.Info.PropertyType))
                // old version: if (propertyInfo.Binding.ObjectType != null)
                {
                    //set simple value
                    propertyInfo.Info.SetValue(element, ConvertXmlValueToObject(assingableValue, propertyInfo.Info.PropertyType), null);
                }
                else
                {
                    if (TypeUtility.IsListType(propertyInfo.Info.PropertyType))
                    //old version: if (propertyInfo.Info.PropertyType.IsGenericType)
                    {
                        ReadXmlPropertiesList(xml, element, propertyInfo);
                    }
                    else
                    {
                        Type elementType = propertyInfo.Binding.ObjectType;
                        if (elementType == null) elementType = propertyInfo.Info.PropertyType;

                        IXomElement newElement = CreateNewXomElement(elementType) as IXomElement;
                        ReadXmlProperties(xmlObject as XElement, newElement, elementType);

                        propertyInfo.Info.SetValue(element, newElement, null);
                    }
                }

                //add property mapping
                element.Properties.Add(propertyInfo.Info.Name, new XomPropertyInfo<XObject>(propertyInfo.Info, new XomNode<XObject>(xmlObject)));
            }

            //register events
            element.PropertyChanged += OnXomElementPropertyChanged;
            element.RootNode = new XomNode<XElement>(xml);
            element.XomCollectionChanged += OnXomCollectionChanged;
        }

        private void ReadXmlPropertiesList(XElement xml, IXomElement element, MappedPropertyInfo propertyInfo)
        {
            //try to cast property to IList
            IList list = (IList)propertyInfo.Info.GetValue(element, null);
            if (list == null) return;

            //check generic type
            Type childType = propertyInfo.Binding.ObjectType;
            if (childType == null) childType = TypeUtility.GetListType(propertyInfo.Info.PropertyType);
            bool isSimpleListType = TypeUtility.IsSimpleType(childType);

            IEnumerable<XElement> childs = xml.Elements(propertyInfo.Binding.XmlName);
            foreach (XElement child in childs)
            {
                object newChildElement;

                // simple list with primitive types
                if (isSimpleListType)
                {
                    newChildElement = ConvertXmlValueToObject(child.Value, childType);
                }
                else
                {
                    //create complex type and read properties
                    newChildElement = CreateNewXomElement(propertyInfo.Binding.ObjectType);
                    ReadXmlProperties(child, newChildElement as IXomElement, propertyInfo.Binding.ObjectType);
                }
                
                list.Add(newChildElement);
            }
        }

        private void OnXomElementPropertyChanged(object sender, PropertyChangedEventArgs propertyChangedEventArgs)
        {
            IXomElement element = sender as IXomElement;
            if (element == null) return;
            //check property name
            if (!element.Properties.ContainsKey(propertyChangedEventArgs.PropertyName)) return;

            //get the new value
            object propertyValue = element.Properties[propertyChangedEventArgs.PropertyName].Info.GetValue(sender, null);
            XObject propertyNode = ((IXomPropertyInfo<XObject>)element.Properties[propertyChangedEventArgs.PropertyName]).Node.Value;

            XomBinding propertyBinding = GetPropertyBinding(sender.GetType(), propertyChangedEventArgs.PropertyName);
            if (propertyBinding == null) return;

            SetXObjectValue(element, ((XomNode<XElement>)element.RootNode).Value, propertyBinding.XmlName, propertyValue, propertyBinding.IsAttribute, propertyBinding.IsRequired);
        }

        private void OnXomCollectionChanged(IXomElement sender, string propertyName, NotifyCollectionChangedEventArgs notifyCollectionChangedEventArgs)
        {
            switch (notifyCollectionChangedEventArgs.Action)
            {
                case NotifyCollectionChangedAction.Add:
                    {
                        AddXomElementToList(sender, propertyName, notifyCollectionChangedEventArgs.NewItems[0], notifyCollectionChangedEventArgs.NewStartingIndex);
                        break;
                    }
                case NotifyCollectionChangedAction.Remove:
                    {
                        RemoveXomElementFromList(sender, notifyCollectionChangedEventArgs.OldItems[0] as IXomElement);
                        break;
                    }
            }
        }

        private void AddXomElementToList(IXomElement parent, string propertyName, object element, int index)
        {
            if (!parent.Properties.ContainsKey(propertyName)) return;
            XomBinding binding = GetPropertyBinding(parent.GetType(), propertyName);
            if (binding == null) return;
            IList parentList = parent.Properties[propertyName].Info.GetValue(parent, null) as IList;
            if (parentList == null) return;

            XElement xmlNode = null;
            IXomElement xomElement = element as IXomElement;
            //simple list type
            if (xomElement == null)
            {
                xmlNode = CreateXElementFromProperty(parent, propertyName, element);
            }
            //complexe list type
            else
            {
                xmlNode = CreateXElementFromObject(xomElement, binding.XmlName);
            }

            if (index >= parentList.Count - 1)
            {
                ((XomNode<XElement>)parent.RootNode).Value.Add(xmlNode);
            }
            else
            {
                XElement child = ((IXomNode<XElement>)((IXomElement)parentList[index+1]).RootNode).Value;
                child.AddBeforeSelf(xmlNode);
            }
        }

        private void RemoveXomElementFromList(IXomElement parent, IXomElement element)
        {
            if (element == null || element.RootNode == null) return;
            if (parent == null || parent.RootNode == null) return;

            ((XElement)element.RootNode).Remove();
        }

        private XElement CreateXElementFromProperty(IXomElement element, string propertyName, object propertyValue)
        {
            if (element == null) return null;
            if (string.IsNullOrEmpty(propertyName)) return null;

            IList<MappedPropertyInfo> propertyInfos = GetOrAddTypeProperties(element.GetType());
            XElement newNode = null;

            foreach (MappedPropertyInfo property in propertyInfos)
            {
                if (property.Info.Name == propertyName)
                {
                    newNode = new XElement(property.Binding.XmlName);
                    newNode.Value = ConvertObjectValueToString(propertyValue);
                    break;
                }
            }

            return newNode;
        }

        private XElement CreateXElementFromObject(IXomElement element, string xmlName)
        {
            if (element == null) return null;
            if (string.IsNullOrEmpty(xmlName)) return null;

            IList<MappedPropertyInfo> propertyInfos = GetOrAddTypeProperties(element.GetType());
            XElement newNode = new XElement(xmlName);

            foreach (MappedPropertyInfo property in propertyInfos)
            {
                object propertyValue = property.Info.GetValue(element, null);
                /*string propertyStringValue = null;
                if (propertyValue != null) propertyStringValue = propertyValue.ToString();*/

                XObject newPropertyNode = null;
                if (TypeUtility.IsSimpleType(property.Info.PropertyType))
                //old version: if (property.Binding.ObjectType != null)
                {
                    newPropertyNode = SetXObjectValue(null, newNode, property.Binding.XmlName, propertyValue, property.Binding.IsAttribute, property.Binding.IsRequired);
                }
                else
                {
                    //List Property
                    if (TypeUtility.IsListType(property.Info.PropertyType))
                    {
                        IList list = (IList)propertyValue;
                        foreach (object listItem in list)
                        {
                            newPropertyNode = CreateXElementFromObject(listItem as IXomElement, property.Binding.XmlName);
                            newNode.Add(newPropertyNode);
                        }
                    }
                    else
                    {
                        newPropertyNode = CreateXElementFromObject(propertyValue as IXomElement, property.Binding.XmlName);
                        XElement parentPropertyNode = newNode.Element(property.Binding.XmlName);
                        if (parentPropertyNode == null)
                        {
                            newNode.Add(newPropertyNode);
                        }
                        else
                        {
                            parentPropertyNode.ReplaceWith(newPropertyNode);
                        }
                    }
                    
                }

                element.Properties.Add(property.Info.Name, new XomPropertyInfo<XObject>(property.Info, new XomNode<XObject>(newPropertyNode)));
            }

            //register events
            element.PropertyChanged += OnXomElementPropertyChanged;
            element.RootNode = new XomNode<XElement>(newNode);
            element.XomCollectionChanged += OnXomCollectionChanged;

            return newNode;
        }

        private MappedPropertyInfo GetMappedPropertyInfoByXmlName(IXomElement parentObject, string xmlName)
        {
            IList<MappedPropertyInfo> propertyInfos = GetOrAddTypeProperties(parentObject.GetType());
            return propertyInfos.FirstOrDefault(property => property.Binding.XmlName == xmlName);
        }

        private XElement GetPreviuseElement(IXomElement parentObject, XElement parent, string newElementName)
        {
            IList<MappedPropertyInfo> propertyInfos = GetOrAddTypeProperties(parentObject.GetType());
            MappedPropertyInfo newinfo = propertyInfos.FirstOrDefault(property => property.Binding.XmlName == newElementName);
            uint newElementIndex = newinfo.Binding.SequenceIndex;
            XElement prevElement = null;
            uint elementIndex = uint.MaxValue;

            foreach (XElement xElement in parent.Elements())
            {
                MappedPropertyInfo info = propertyInfos.FirstOrDefault(property => property.Binding.XmlName == xElement.Name.LocalName);
                if (info == null) continue;
                if (info.Binding.SequenceIndex > newinfo.Binding.SequenceIndex && info.Binding.SequenceIndex <= elementIndex)
                {
                    elementIndex = info.Binding.SequenceIndex;
                    prevElement = xElement;
                }
            }

            return prevElement;
        }

        private XObject SetXObjectValue(IXomElement parentObject, XElement parent, string name, object value, bool isAttribute, bool isRequired)
        {
            if (isAttribute)
            {
                if (isRequired && value == null)
                {
                    //null values deletes attributes
                    value = string.Empty;
                }

                XAttribute attribute = parent.Attribute(name);
                if (attribute == null)
                {
                    if (value == null) return null;
                    parent.Add(new XAttribute(name, ConvertObjectValueToString(value)));

                    if (parentObject!=null)
                    {
                        //linqtoxml has no insert attribute function -> reorder attributes after add
                        //find right index
                        IList<Tuple<string, string, string, uint>> metaInfos = new List<Tuple<string, string, string, uint>>();
                        foreach (XAttribute xAttribute in parent.Attributes())
                        {
                            string propertyName = string.Empty;
                            uint sequenceIndex = uint.MaxValue;
                            MappedPropertyInfo mappedPropertyInfo = GetMappedPropertyInfoByXmlName(parentObject, xAttribute.Name.LocalName);
                            if (mappedPropertyInfo != null)
                            {
                                propertyName = mappedPropertyInfo.Info.Name;
                                sequenceIndex = mappedPropertyInfo.Binding.SequenceIndex;
                            }

                            metaInfos.Add(new Tuple<string, string, string, uint>(xAttribute.Name.LocalName, propertyName, xAttribute.Value, sequenceIndex));
                            //AddMetaInfosToAttribute(parentObject, xAttribute);
                        }

                        IOrderedEnumerable<Tuple<string, string, string, uint>> sortedMetaInfos = metaInfos.OrderBy(t => t.Item4);
                        parent.RemoveAttributes();

                        foreach (Tuple<string, string, string, uint> metaInfo in sortedMetaInfos)
                        {
                            XAttribute newAtt = new XAttribute(metaInfo.Item1, metaInfo.Item3);
                            parent.Add(newAtt);
                            if (!string.IsNullOrEmpty(metaInfo.Item2))
                            {
                                ((IXomPropertyInfo<XObject>) parentObject.Properties[metaInfo.Item2]).Node.Value = newAtt;
                            }
                        }
                    }
                }
                else
                {
                    parent.SetAttributeValue(name,ConvertObjectValueToString(value));
                    //attribute.Value = ConvertObjectValueToString(value);
                }

                return parent.Attribute(name);
            }
            else
            {
                //if xmlName is empty than set the root value
                if (string.IsNullOrEmpty(name))
                {
                    if (isRequired && value == null)
                    {
                        //null values deletes attributes
                        value = string.Empty;
                    }

                    parent.Value = ConvertObjectValueToString(value);
                    
                    return parent;
                }
                else
                {
                    if (isRequired && value == null)
                    {
                        //null values deletes attributes
                        value = string.Empty;
                    }

                    XElement element = parent.Element(name);
                    if (element == null)
                    {
                        if (value == null) return null;
                        XElement newElement;

                        if (value is IXomElement)
                        {
                            newElement = CreateXElementFromObject(value as IXomElement, name);
                        }
                        else
                        {
                            newElement = new XElement(name, ConvertObjectValueToString(value));
                        }

                        if (parentObject == null)
                        {
                            parent.Add(newElement);
                        }
                        else
                        {
                            XElement prevElement = GetPreviuseElement(parentObject, parent, name);
                            if (prevElement == null)
                            {
                                parent.Add(newElement);
                            }
                            else
                            {
                                prevElement.AddBeforeSelf(newElement);
                            }
                        }
                    }
                    else
                    {
                        if (value is IXomElement)
                        {
                            element.ReplaceWith(CreateXElementFromObject(value as IXomElement, name));
                        }
                        else
                        {
                            parent.SetElementValue(name, ConvertObjectValueToString(value));
                        }
                    }

                    return parent.Element(name);
                }
            }
        }
    }
}
